﻿using System;
using Notoric.Commanding.Holders;

namespace Notoric.Commanding.CommandBase
{
    /// <summary>
    /// Defines the basic interface and implements common functionality of the Notoric commands.
    /// </summary>
    /// <remarks>
    /// <para>
    /// This class implements the management of command parameter set and the binding of parameter values to subcommands.
    /// </para>
    /// </remarks>
    public abstract class CommandBase : System.Windows.Input.ICommand
    {
        #region ICommand Members

        /// <summary>
        /// Determines whether the command can be executed in its current state.
        /// </summary>
        /// <remarks>
        /// In the basic implementation the command can execute when the values of all of its input parameters can be computed.
        /// Refer to <see cref="Parameter.CanComputeValue"/> for details.
        /// </remarks>
        /// <param name="parameter">
        /// Data used by the command. If the command does not require data to be passed, this object can be set <see langword="null"/>.
        /// </param>
        /// <returns><see langword="true"/> if the command can execute, <see langword="false"/> otherwise</returns>
        public virtual bool CanExecute(object parameter)
        {
            bool canExecute = true;

            foreach (Parameter param in parameterSet)
            {
                if (param.Direction == ParameterDirection.Out)
                    continue;

                canExecute &= param.CanComputeValue(parameter);

                if (!canExecute)
                    break;
            }

            return canExecute;
        }

        /// <summary>
        /// Occurs when changes occur that affect whether or not the command should execute.
        /// </summary>
        /// <remarks>
        /// Normally, a command source calls <see cref="CanExecute"/> on the command when this event occurs.<br/>
        /// Normally, if the command cannot execute, the command source disables itself.
        /// </remarks>
        public event EventHandler CanExecuteChanged;

        /// <summary>
        /// Executes the command.
        /// </summary>
        /// <param name="parameter">
        /// Data used by the command. If the command does not require data to be passed, this object can be set to <see langword="null"/>.
        /// </param>
        /// <exception cref="ExecutionException">
        /// An unhandled exception occured during the execution.
        /// </exception>
        public abstract void Execute(object parameter);

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets or sets the name of the command.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="string"/>
        /// </para>
        /// <para>
        /// The name helps user to identify the command action.
        /// </para>
        /// </value>
        public string Name
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the timestamp of the last successful command execution.
        /// </summary>
        public DateTime LastExecuted
        {
            get;
            protected set;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Adds a new parameter to the parameter set of this command.
        /// </summary>
        /// <param name="name">Name of the new parameter</param>
        /// <param name="direction">Direction of the parameter. One of the <see cref="ParameterDirection"/> values</param>
        /// <exception cref="ParameterExistsException">
        /// If there already is a parameter with the same name in the command parameter set.
        /// </exception>
        public void AddParameter<T>(string name, ParameterDirection direction)
        {
            parameterSet.AddParameter(name, direction, new ValueHolder<T>());
        }

        /// <summary>
        /// Checks whether the value of a given parameter can be computed or not.
        /// </summary>
        /// <param name="name">Name of the parameter</param>
        /// <param name="parameter">Parameter for the command execution</param>
        /// <returns><see langword="true"/> if the parameter value can be computed, <see langword="false"/> otherwise</returns>
        /// <remarks>
        /// The value of the parameter can be computed if it is either known or bound to another command that can execute with the given execution parameter.
        /// </remarks>
        public bool CanComputeParameterValue(string name, object parameter)
        {
            Parameter param = parameterSet.GetParameter(name);
            if (param == null)
                throw new ParameterDoesNotExistException(String.Format("The requested parameter {0} does not exist!", name));

            return param.CanComputeValue(parameter);
        }

        /// <summary>
        /// Gets the value of a given parameter.
        /// </summary>
        /// <typeparam name="T">Type of the parameter value</typeparam>
        /// <param name="name">Name of the parameter whose value is being searched</param>
        /// <returns>Value of the parameter</returns>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter does not exist.
        /// </exception>
        /// <exception cref="InvalidCastException">
        /// The parameter value has a different type.
        /// </exception>
        public T GetParameterValue<T>(string name)
        {
            return parameterSet.GetParameterValue<T>(name);
        }

        /// <summary>
        /// Gets the value of a given parameter as a reference to <see cref="object"/>.
        /// </summary>
        /// <param name="name">Name of the parameter whose value is being searched</param>
        /// <returns>Value of the parameter</returns>
        /// <exception cref="ParameterDoesNotExistException">
        /// Parameter with the given name does not exist.
        /// </exception>
        /// <exception cref="ValueNotKnownException">
        /// The value of the parameter is not known (i.e. the state of the value is <see cref="ValueState.Unknown"/>).
        /// </exception>
        public object GetUntypedParameterValue(string name)
        {
            return parameterSet.GetUntypedParameterValue(name);
        }

        /// <summary>
        /// Sets the value of a parameter.
        /// </summary>
        /// <remarks>
        /// Calling this method will raise the <see cref="CanExecuteChanged"/> event on this instance.
        /// </remarks>
        /// <typeparam name="T">Type of the parameter value</typeparam>
        /// <param name="name">Name of the parameter whose value is being set</param>
        /// <param name="value">New value of the parameter</param>
        /// <exception cref="ParameterDoesNotExistException">
        /// Parameter with the given name does not exist.
        /// </exception>
        /// <exception cref="InvalidCastException">
        /// The parameter value does not have the same type as the value that was passed.
        /// </exception>
        public void SetParameterValue<T>(string name, T value)
        {
            parameterSet.SetParameterValue<T>(name, value);
            
            RaiseCanExecuteChanged();
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Gets a reference to the parameter with a given name.
        /// </summary>
        /// <param name="name">Name of the parameter that is being searched</param>
        /// <returns>Reference to the parameter with the given name or null if no such parameter exists.</returns>
        internal Parameter GetParameter(string name)
        {
            return parameterSet.GetParameter(name);
        }

        #endregion

        #region Protected Methods

        /// <summary>
        /// Binds a value of a given parameter of this command to a value of a given parameter on another command.
        /// </summary>
        /// <param name="name">Name of the parameter of this command whose value is being bound to another command</param>
        /// <param name="command">Command to which the value is being bound</param>
        /// <param name="outputName">Name of the parameter of the command to which the value is being bound</param>
        /// <exception cref="ParameterDoesNotExistException">
        /// The parameter being bound does not exist on this command or the target parameter does not exist on the other command.
        /// </exception>
        /// <exception cref="IncompatibleTypesException">
        /// The parameters being bound do not have compatible value types.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Some of the parameters is null.
        /// </exception>
        /// <exception cref="InvalidOperationException">
        /// The parameter beind bound (i.e. the one identified by <paramref name="name"/>) is output.
        /// </exception>
        protected void BindParameterToCommandInternal(string name, CommandBase command, string outputName)
        {
            parameterSet.BindParameterToCommand(name, command, outputName);

            command.CanExecuteChanged += SubCommand_CanExecuteChanged;
        }

        /// <summary>
        /// Occurs when the <see cref="CanExecuteChanged"/> event is raised on a subcommand of this command.
        /// </summary>
        /// <param name="sender">The command instance on which the event was raised</param>
        /// <param name="args">Event data</param>
        /// <remarks>
        /// In this basic implementation, the handler simply raises the <see cref="CanExecuteChanged"/> event on this instance as well.
        /// </remarks>
        protected virtual void SubCommand_CanExecuteChanged(object sender, EventArgs args)
        {
            RaiseCanExecuteChanged();
        }

        /// <summary>
        /// Raises the <see cref="CanExecuteChanged"/> event on this <see cref="System.Windows.Input.ICommand"/> interface instance.
        /// </summary>
        protected void RaiseCanExecuteChanged()
        {
            if (CanExecuteChanged != null)
                CanExecuteChanged(this, EventArgs.Empty);
        }

        #endregion

        #region Protected Fields

        /// <summary>
        /// Holds the parameter set of this command.
        /// </summary>
        protected ParameterSet parameterSet = new ParameterSet();

        #endregion
    }
}
